package com.google.code.jwebannotation;

import java.lang.reflect.Method;
import java.net.URL;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.scannotation.AnnotationDB;
import org.scannotation.WarUrlFinder;

import com.google.code.jwebannotation.annotation.ErrorPage;
import com.google.code.jwebannotation.annotation.Lifecycle;
import com.google.code.jwebannotation.annotation.Request;
import com.google.code.jwebannotation.annotation.Stage;

/**
 * Application Lifecycle Listener implementation class ControllerSetup
 * 
 */
public class WebAnnotationLifecycle implements ServletContextListener {

	private Log log = LogFactory.getLog(WebAnnotationLifecycle.class);

	private LinkedList<InternalWrapper> list;

	public void contextInitialized(ServletContextEvent arg0) {
		log.info("Starting simple framework VERSION " + WebAnnotation.VERSION);

		LinkedList<URL> urlList = new LinkedList<URL>();
		URL[] urls = WarUrlFinder.findWebInfLibClasspaths(arg0.getServletContext());
		for (URL u : urls) {
			urlList.add(u);
		}
		urlList.add(WarUrlFinder.findWebInfClassesPath(arg0.getServletContext()));

		log.debug("Looking for classes in urls '" + urlList + "'");
		AnnotationDB ann = new AnnotationDB();
		try {
			ann.scanArchives(urlList.toArray(new URL[0]));
			Map<String, Set<String>> map = ann.getAnnotationIndex();
			if (map != null) {
				Set<String> set = map.get(Request.class.getName());
				log.debug("Request Annotations found: " + set);
				if (set != null) {
					parseRequest(set);
				}

				set = map.get(Lifecycle.class.getName());
				log.debug("Lifecycle Annotations Found: " + set);
				if (set != null) {
					parseLifecyle(set, arg0);
				}

				set = map.get(ErrorPage.class.getName());
				log.debug("ErrorPage Annotations Found: " + set);
				if (set != null) {
					parseErrorPage(set, arg0);
				}
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	private void parseErrorPage(Set<String> set, ServletContextEvent arg0)
			throws ClassNotFoundException, InstantiationException, IllegalAccessException {
		if (set.size() > 1) {
			throw new RuntimeException("Multiple error pages are not allowd. Error pages found: "
					+ set);
		}
		for (String s : set) {
			Class<?> clazz = Class.forName(s);
			Object o = clazz.newInstance();
			for (Method method : clazz.getMethods()) {

				ErrorPage lc = method.getAnnotation(ErrorPage.class);
				if (lc != null) {

					Class<?>[] params = method.getParameterTypes();
					if (params.length != 3 || !params[2].equals(Exception.class)
							|| !(params[0].equals(HttpServletRequest.class))
							|| !(params[1].equals(HttpServletResponse.class))) {
						throw new RuntimeException(
								"Method '"
										+ method
										+ "' must have a siganture of '(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse,java.lang.Exception)'");
					}

					if (!method.getReturnType().equals(String.class)) {
						throw new RuntimeException("Method '" + method
								+ "' must have a return type of String.");
					}

					log.trace("Mapping Annotation '" + lc + "' to " + method);

					InternalWrapper wrapper = new InternalWrapper();
					wrapper.setObject(o);
					wrapper.setObjectMethod(method);
					WebAnnotationServlet.errorPage = wrapper;
					return;
				}
			}
		}
	}

	private void parseLifecyle(Set<String> set, ServletContextEvent arg0)
			throws ClassNotFoundException, InstantiationException, IllegalAccessException {
		list = new LinkedList<InternalWrapper>();
		for (String s : set) {
			Class<?> clazz = Class.forName(s);
			Object o = clazz.newInstance();
			for (Method method : clazz.getMethods()) {

				Lifecycle lc = method.getAnnotation(Lifecycle.class);
				if (lc != null) {

					Class<?>[] params = method.getParameterTypes();
					if (params.length != 1 || !params[0].equals(ServletContextEvent.class)) {
						throw new RuntimeException(
								"Method '"
										+ method
										+ "' must have a siganture of '(javax.servlet.ServletContextEvent)'");
					}

					log.trace("Mapping Annotation '" + lc + "' to " + method);

					InternalWrapper wrapper = new InternalWrapper();
					wrapper.setObject(o);
					wrapper.setObjectMethod(method);
					wrapper.setStage(lc.value());
					list.add(wrapper);
				}
			}
		}
		invokeStage(Stage.STARTUP, arg0);
	}

	public void parseRequest(Set<String> set) throws ClassNotFoundException,
			InstantiationException, IllegalAccessException {
		WebAnnotationServlet.map = new HashMap<String, InternalWrapper>();
		for (String s : set) {
			Class<?> clazz = Class.forName(s);
			Object o = clazz.newInstance();
			for (Method method : clazz.getMethods()) {

				Request model = method.getAnnotation(Request.class);

				if (model != null) {

					Class<?>[] params = method.getParameterTypes();
					if (!(params[0].equals(HttpServletRequest.class))
							|| !(params[1].equals(HttpServletResponse.class))) {
						throw new RuntimeException(
								"Method '"
										+ method
										+ "' must have a siganture of '(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse,Bean...)'");
					}
					if (!method.getReturnType().equals(String.class)) {
						throw new RuntimeException("Method '" + method
								+ "' must have a return type of String.");
					}
					log.trace("Mapping Annotation '" + model + "' to " + method);

					InternalWrapper wrapper = new InternalWrapper();
					wrapper.setObject(o);
					wrapper.setHttpMethod(model.method());
					wrapper.setPage(model.page());
					wrapper.setObjectMethod(method);

					LinkedList<Class<?>> list = new LinkedList<Class<?>>();
					for (int i = 2; i < params.length; i++) {
						log.trace("Adding bean '" + params[i] + "'");
						list.add(params[i]);
					}
					wrapper.setBean(list);
					WebAnnotationServlet.map.put(model.page(), wrapper);
				}
			}

		}
	}

	public void contextDestroyed(ServletContextEvent arg0) {
		invokeStage(Stage.SHUTDOWN, arg0);
		WebAnnotationServlet.map = null;
	}

	private void invokeStage(Stage stage, ServletContextEvent arg0) {
		log.trace("Invoking stage: " + stage);
		if (list == null)
			return;
		for (InternalWrapper mw : list) {
			if (mw.getStage().equals(stage)) {
				try {
					log.trace("Invoking method '" + mw.getObjectMethod() + "'");
					mw.getObjectMethod().invoke(mw.getObject(), arg0);
				} catch (Exception e) {
					log.error("Lifecycle stage caused an exception", e);
				}
			}
		}
	}

}
